.. _Строки:

Строки
======

.. index::
    string
    строка

Мы использовали строки в различных программах, с которыми сталкивались до сих пор. Строки представляют собой
комбинацию двух функций, которые мы рассмотрели в последних трех главах:
символы и массивы. В простейшем определении строки - это не что иное, как массивы
символов то есть char[] - это тип строки.
Это простое определение может ввести в заблуждение. Как мы видели в главе "Символы
" (:ref:`глава символы<глава символы>`), D имеет три отдельных типа символов. Массивы этих символьных
типов приводят к трем отдельным строковым типам, некоторые из которых могут иметь неожиданные
результаты при некоторых строковых операциях.

Функции readln and strip, вместо функции readf
----------------------------------------------

.. index::
    strip
    readf

Даже при чтении строк из терминала возникают неожиданные результаты.
Будучи символьными массивами, строки также могут содержать управляющие символы, наример '\n'.
При считывании строк из входных данных управляющий символ, соответствующий
клавише Enter, нажатой в конце ввода, также становится частью строки.
Кроме того, поскольку нет способа указать функции read(), сколько символов нужно
прочитать, она продолжает считывать до конца всего ввода. По этим причинам
readf() не работает должным образом при чтении строк. Рассмотрим программу

.. code-block:: d

    import std.stdio;

    void main()
    {
        char[] name;
        write("What is your name? ");
        readf(" %s", &name);
    }



Клавиша Enter, нажатая пользователем, не завершает ввод. Функция readf() - продолжает ожиждать
ввод следующих символов, чтобы присоеденить их к результирующей строке.

.. code-block:: console

    What is your name? Mert
         Ввод не завершен, нажатием клавиши Enter
         Нажимаем Enter уще раз

Один из способов закрыть стандартный поток ввода на терминале - это нажать комбинацию
клавиш Ctrl-D (в Unix-based OS) или Ctrl-Z (в Windows OS). Если пользователь завершит ввод
подобным образом, мы увидим, что символ новой строки стал частью результата ввода

.. code-block:: console

    Hello Mert
        ← новая строка после ввода имени
    !   ← еще одна новая строка, перед закрытием входного потока

Восклицательный знак появляется после этих символов вместо того, чтобы быть напечатанным
сразу после имени

Функция readln() лучше подходит для чтения строки. readln - сокращение от  "read line",
readln() читает входные символы до появления в потоке символа новой строки.В данном случае строка формата и & оператор не нужны

.. index::
    readln
    char

.. code-block:: d

    import std.stdio;

    void main()
    {
        char[] name;
        write("What is your name? ");
        readln(name);
        writeln("Hello ", name, "!");
    }


Однако функция readln() так же запоминает символ "new-line". Это делается для того, чтобы у программы был
способ определить, состоял ли ввод из полной строки или был достигнут конец ввода:

.. code-block:: console

    What is your name? Mert
    Hello Mert
    ! ← new-line character before the exclamation mark

Such control characters as well as all whitespace characters at both ends of
strings can be removed by std.string.strip :

Подобные управляющие символы, такие как пробелы, с обоих концов строк, могут быть
удалены при помощи функции strip определенной в модуле std.string

.. code-block:: d

    import std.stdio;
    import std.string;

    void main()
    {
        char[] name;
        write("What is your name? ");
        readln(name);
        name = strip(name);
        writeln("Hello ", name, "!");
    }


The strip() expression above returns a new string that does not contain the
trailing control characters. Assigning that return value back to name produces the
intended output:

Функция strip возвращает новую строку, в которой удалены завершающие управляющие символы
В данном варианте мы присваеваем новую строку той же переменной

Вывод

.. code-block:: console

    What is your name? Mert
    Hello Mert!         ← управляющие символы удалены

readln() can be used without a parameter. In that case it returns the line that it
has just read. Chaining the result of readln() to strip() enables a shorter and
more readable syntax:

Функция readln может быть использована без параметров. В этом случае йункция возвращает прочитанную строку
Используя комбинацию readln и strip получим компактный читабельный код

..  code-block::d

    string name = strip(readln());

Я начну использовать эту форму после введения типа string ниже.

Использование функции formattedRead для разбора строк
-----------------------------------------------------

.. index::
    read
    formattedRead

Строка считанна из входных данных или из любого другого источника, может быть проанализирована.
Отдельные данные могут быть преобразованы помощью функции formatted Read() определенной в модуле std.format.
Его первым параметром является строка содержащая данные, а остальные параметры используются точно так же, как readf() :

.. code-block:: d

    import std.stdio;
    import std.string;
    import std.format;

    void main()
    {
        write("Please enter your name and age," ~ " separated with a space: ");
        string line = strip(readln());
        string name;
        int age;
        formattedRead(line, " %s %s", name, age);
        writeln("Your name is ", name, ", and your age is ", age, '.');
    }

.. code-block:: console

    Please enter your name and age, separated with a space: Mert 30
    Your name is Mert, and your age is 30.

Как read(), так и formattedRead() возвращают количество элементов, которые они смогли
успешно проанализировать и преобразовать. Это значение можно сравнить с ожидаемым
количеством элементов данных, чтобы можно было проверить вводимые данные. Например, поскольку
приведенный выше вызов formattedRead() ожидает чтения двух элементов (строка в качестве имени и
int в качестве возраста), следующая проверка гарантирует, что это действительно так:

.. code-block:: d

    uint items = formattedRead(line, " %s %s", name, age);
    if (items != 2)
    {
        writeln("Error: Unexpected line.");
    }
    else
    {
        writeln("Your name is ", name, ", and your age is ", age, '.');
    }


When the input cannot be converted to name and age , the program prints an
error:

Если ввод не может быть преобразован в два элемента имя и возраст. Программа напечатает сообщение об ошибке

.. code-block:: console

   Пожалуйста, введите ваше имя и возраст, разделив их пробелом: Mert
   Ошибка: Ошибочная строка.

Двойные кавычки, но не одинарные
---------------------------------

Мы видели, что одинарные кавычки используются для обозначения символьных литералов.
Строковые литералы определяются двойными кавычками.То есть 'a' - это символ, "a" - это строка
содержащая один символ.

Строковые типы string, wstring, and dstring являются неизменяемыми
------------------------------------------------------------------

.. index::
    string
    wstring
    dstring
    char
    wchar
    dchar

Язык D поддерживает 3 строковых типа в соответсвии с символьными типами

* string --> char[]
* wstring --> wchar[]
* dstring --> dchar[]

Символы содержащиеся в этих строках не могут быть изменены.
Например, символы в массиве типа wchar[] могут быть изменены, но символы в сстроке wstring - не меняются
(Мы рассомтрим концепцию неизменности языка  D в последующих главах)
Рассмотрим пример, в котором пытаются изменить регистр первого символа в строке, в данном случае произойдет
ошибка компиляции

.. code-block:: d

    string cannotBeMutated = "hello";
    cannotBeMutated[0] = 'H'; // ← ошибка компиляции

We may think of defining the variable as a char[] instead of the string alias but
that cannot be compiled either:

Можем подумать если мы определим строку как массив символов вместо строки, то ошибки не будет
Нет и в этом случае произойдет ошибка компиляции

.. code-block:: d

    char[] a_slice = "hello"; // ← compilation ERROR

На этот раз ошибка компиляции вызвана сочетанием двух факторов:

#. Тип строковых литералов, таких как "hello", - string , а не char[], поэтому они
неизменяемы.
#. Символ [] в левой части - это срез, который, если код скомпилирован,
предоставил бы доступ ко всем символам правой части.
Поскольку char[] является изменяемым, а string - нет, возникает несоответствие. Компилятор
не разрешает доступ к символам неизменяемого массива через изменяемый срез
Решение здесь состоит в том, чтобы создать копию неизменяемой строки с помощью свойства .dup

.. code-block:: d

    import std.stdio;
    void main()
    {
        char[] s = "hello".dup;
        s[0] = 'H';
        writeln(s);
    }

Программа будет успешно скомпилтрована и напечатает изменнёную строку

.. code-block::  console

     Hello

Соответсвенно,массив char[] не может быть использован если строковый тип необходим.
В подобных случаях свойство idup может быть использовано для получения неизменяемой строки из
изменяемого символьного массива. Например,если s переменная типа char[], следующий код потерпит
неудачу при компиляции.

.. code-block:: d

    string result = s ~ '.'; // ← ошибка компиляции

Если тип переменной s есть char[], то тип выражения с правой стороны есть char[]
Свойство idup  используется для получения неизменяемой строки из существующей строки.

.. code-block:: d

    string result = (s ~ '.').idup; // ← теперь компилируется

Возможные проблемы связанные с понятием длины строки
----------------------------------------------------

.. index::
    длина строки
    length

Мы видели что для представления символа  в кодировке Unicode, может потребоваться более
одного байта.Например символ 'é' (латинская буква e в комбинации с надстрочным знаком)
В кодировке Unucode представляется двумя байтами. Этот факт отражается на реализации свойсства
.length

.. code-block:: d

    writeln("résumé".length);

Although "résumé" contains six letters, the length of the string is the number of
UTF-8 code units that it contains:

Строка "résumé" содержит 6 букв, но длина строки равна количеству кодовых едениц Unicode и она равна

.. code-block:: console

    8

Типом элементов строковых литералов, таких как "hello", является char, и каждое символьное
значение представляет кодовую единицу UTF-8. Рассмотрим проблему, которая может возникнуть при попытке
заменить символ из двух кодовых единиц символом из одной кодовой единицы:

.. code-block:: d

    char[] s = "résumé".dup;
    writeln("Before: ", s);
    s[1] = 'e';
    s[5] = 'e';
    writeln("After : ", s);


Два символа  'e' не заменят два символа 'é', они заменят одиночные кодовые еденицы.
Результатом будет непраильная Unicode кодировка

.. code-block:: console

    Before: résumé
    After : re�sueé


Если мы хотим оперировать с буквами, символами в Unicode кодировке непосредствеенно,
как в только что предложеном фрагменте кода, необходимо использовать тип dchar

.. code-block:: d

    dchar[] s = "résumé"d.dup;
    writeln("Before: ", s);
    s[1] = 'e';
    s[5] = 'e';
    writeln("After : ", s);

Вывод

.. code-block:: console

    Before: résumé
    After : resume

Пожалуйста, обратите внимание на два отличия в новом коде:

#. Тип строки - dchar[] .
#. В конце литерала "резюме" стоит буква d, указывающая его тип в виде массива
   dchar s.

В любом случае, имейте в виду, что использование dchar[] и dstring не решает всех
проблем манипулирования символами Unicode. Например, если пользователь
вводит текст "résumé", вы и ваша программа не можете предположить, что длина строки
будет равна 6 даже для строк dchar. Это может быть больше, если, например, по крайней мере один из символов
"é" закодирован не как отдельная кодовая точка, а как комбинация "e"
и несамостоятельных надстрочных знаков. Избежать контакта с этой и многих других
Проблемы с Unicode, можно использовать безопасные манипуляуии с текстом предлагаемые библиотекой  Unicode.

Строковые литералы
-------------------

Необязательный символ, который указывается после строковых литералов, определяет тип
элементов строки:

.. code-block:: d

    import std.stdio;

    void main()
    {
        string s = "résumé"c; // то же самое что "résumé"
        wstring w = "résumé"w;
        dstring d = "résumé"d;
        writeln(s.length);
        writeln(w.length);
        writeln(d.length);
    }

Вывод

.. code-block:: console

    8
    6
    6

Поскольку все символы Юникода в "резюме" могут быть представлены одним
символом wchar или dchar , длина последних двух равна количеству символов.

Слияние строк (конкатенация)
----------------------------

.. index::
    слияние строк
    конкатенация


Поскольку строки есть по сути массивы, то все операции с массивами могут быть исподьзованы по отношению к строкам.
~ конкатенация двух строк и ~= добавляет строку к существующей строке

.. code-block:: d

    import std.stdio;
    import std.string;

    void main()
    {
        write("What is your name? ");
        string name = strip(readln());
        // Конкатенация:
        string greeting = "Hello " ~ name;
        // Добавление:
        greeting ~= "! Welcome...";
        writeln(greeting);
    }


Вывод

.. code-block:: console

    What is your name? Can
    Hello Can! Welcome...

Сравнение строк
---------------

*Примечание: Unicode не определяет порядок расположения символов, только их коды в Unicode.
По этой причине ниже вы можете получить результаты, которые не соответствуют вашим ожиданиям.*

Мы использовали операторы сравнения < , >= и т.д. с целыми числами и с числами сплавающей точкой до этого
Эти же операторы могут быть использованы и со строками, но есть сысловое различие. Строки сравниваются в лексикографическом порядке
При таком упорядочении код Юникода каждого символа соответствует его месту в гипотетическом большом алфавите Юникода.
Понятия "меньше" и "больше" заменены в этом гипотетическом алфавите на понятия "до" и "после":

.. code-block:: d

    import std.stdio;
    import std.string;

    void main()
    {
        write("Enter a string: ");
        string s1 = strip(readln());
        write("Enter another string: ");
        string s2 = strip(readln());
        if (s1 == s2)
        {
            writeln("They are the same!");
        }
        else
        {
            string former;
            string latter;
            if (s1 < s2)
            {
                former = s1;
                latter = s2;
            }
            else
            {
                former = s2;
                latter = s1;
            }
        }
        writeln("'", former, "' comes before '", latter, "'.");
    }


Поскольку Unicode использует буквы основного латинского алфавита из таблицы ASCII,
строки, содержащие только буквы таблицы ASCII, всегда будут упорядочены правильно.

Верхний и нижний регистры различны
----------------------------------


Поскольку каждый символ имеет уникальный код, каждый вариант буквы отличается от
других. Например, "A" и "a' - это разные буквы при непосредственном сравнении строк в Юникоде.
Кроме того, в соответствии со значениями ASCII-кода все латинские
заглавные буквы сортируются перед всеми строчными буквами. Например, "B"
стоит перед "a". Функция icmp() модуля std.string может использоваться, когда строки необходимо сравнивать независимо от регистра.
Вы можете ознакомиться с функциями этого модуля в его онлайн-документации.
Поскольку строки являются массивами (и, как следствие, диапазонами), функции модулей
std.array , std.algorithm и std.range также очень полезны для работы со строками

Упражнение
----------

#. Просмотрите документацию модулей std.string, std.array, std.algorithm и std.range.
#. Напишите программу, которая использует оператор ~: Пользователь вводит
   имя и фамилию, все строчными буквами. Введите полное имя,
   содержащее правильные заглавные буквы имени и фамилии. Например,
   когда строками являются "ebru" и "domates", программа должна напечатать
   "Ebru Domates".
#. Прочитайте строку из входных данных и выведите часть между первой и последней буквами "e"
   в строке. Например, когда строка звучит так: "в этой строке пять слов", то
   программа должна вывести "e имеет пять".
   Вы можете найти функции indexOf() и lastIndexOf() полезными для получения
   двух индексов, необходимых для создания среза.
   Как указано в их документации, возвращаемыми типами indexOf() и
   lastIndexOf() являются не int и не size_t , а ptrdiff_t . Возможно, вам придется
   определить переменные именно этого типа:    ptrdiff_t first_e = indexOf(строка, 'e');
   Можно определить переменные с помощью ключевого слова auto, которое мы увидим в
   следующей главе: auto first_e = indexOf(строка, 'e');
